Mosquitto(MQTTS)サーバをACMによるサーバ証明書で構築してみました
1 はじめに
製造ビジネステクノロジー部の平内(SIN)です。
メッセージブローカーのMosquittoをMQTTS(TLS:ポート8883)で構築する際は、サーバ証明書が必要となりますが、多くの場合、自己署名証明書(オレオレ証明書)を使用する方法が紹介されています。
今回は、正規のドメイン名で、ACM(AWS Certificate Manager)で発行した証明書を使用して構築する手順を試してみました。
方法としては、Mosquittoの手前にNLBを配置し、ここでTLSを終端しています。Mosquitto本体は、TLS無しのMQTT:ポート1883で起動しています。
ACMを使用する場合、秘密鍵の管理や、証明書の更新は必要なくなります。
また、独自に作成したサーバ証明書では、これを検証するためのCA証明書もクライアント側に配置することになりますが、パブリックな名前を使用するので、これも必要なくなります。
TLSでは、そのバージョンや、対応暗号方式が問題になることがありますが、今回の方式では、終端がNLBとなるため、Mosquittoの仕様にはまったく依存せず、ELBのセキュリティポリシーで対応することになります。
参考:Network Load Balancer のセキュリティポリシー
2 構築
図のような構成で、Route53及び、ACM以外の部分をCDKとして作成しましたので、そのコードを使用して作業を進めさせていただきます。
(1) ACMによる証明書作成
ここでは、サーバ名を「mqtt.alexa-dev.tokyo」としています。
ドメインを同一アカウント(Route53)で管理している場合、ACMのコンソールから、証明書をリクエスト / パブリック証明書をリクエスト / DNS検証 / Route53でレコードを作成 の手順で数十秒で作成できてしまいます。
参考:AWS Certificate Manager でのパブリック証明書のリクエスト
参考:AWS Certificate Manager パブリック証明書のドメインの所有権を検証する
証明書の作成が完了したら、ARNをコピーしておきます。
(2) CDKデプロイ
Githubからコードをcloneします
% git clone https://github.com/furuya02/mqtt-attach-acm-to-nlb.git
% cd mqtt-attach-acm-to-nlb
cdk/cdk.jsonを開いて、下記の2つを編集します。
key | value |
---|---|
accountId | デプロイするアカウントID |
certificateArn | 先に作成した証明書のARN |
{
"app": "npx ts-node --prefer-ts-exts bin/cdk.ts",
"watch": {
},
"context": {
"projectName": "mqtt-attach-acm-to-nlb",
"accountId": "xxxxxxxxxxxx",
"vpcCidr": "10.0.0.0/16",
"taskCpu": 256,
"taskMemory": 512,
"desiredCount": 0,
"natGateways": 0,
"certificateArn": "arn:aws:acm:ap-northeast-1:xxxxxxxxxxxx:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
cdkフォルダに移動して、CDKをdeployします。
この時点では、「"desiredCount": 0」となっているため、起動するタスク数は、0個となります。
% cd cdk
% npm install
% cdk diff mqtt-attach-acm-to-nlb-stack -c tag=latest
% cdk deploy mqtt-attach-acm-to-nlb-stack -c tag=latest
% cd ..
EC2のコンソールから、deployされたロードバランサーを確認し、DNS名をコピーしておきます。
(3) DockerイメージのPush
下記のコマンドで、Dockerイメージがビルドされ、ECRにプッシュされます。
% node deploy.js latest
docker push 123456781234.dkr.ecr.ap-northeast-1.amazonaws.com/mqtt-attach-acm-to-nlb-repo:latest
finish.
(4) Route53へのレコード登録(Aレコード Alias使用)
下記の内容で、レコードを追加します。
項目 | 値 |
---|---|
レコードタイプ | A レコード |
エイリアス | 有効 |
レコード名 | mqtt(mqtt.ドメイン名)がサーバのホスト名となります |
エンドポイント | Network Load Blancerへのエリアス |
リージョン | ap-noetheast-1 |
Network Load Blancer | 先に作成したNLBのDNS名を選択 |
レコードの追加が完了すると、サーバ名でも、NLBのDNS名でも、同じIPアドレスが引けるようになります。
% dig mqtt-attach-acm-to-nlb-nlb-5429fb2fca447d42.elb.ap-northeast-1.amazonaws.com
;; ANSWER SECTION:
mqtt-attach-acm-to-nlb-nlb-5429fb2fca447d42.elb.ap-northeast-1.amazonaws.com. 60 IN A 43.206.49.94
% dig mqtt.alexa-dev.tokyo
;; ANSWER SECTION:
mqtt.alexa-dev.tokyo. 60 IN A 43.206.49.94
※ エイリアスが利用できない場合は、CNAME登録となります
(5) タスク起動
cdk/cdk.jsonを編集して、desiredCountの0を1に変更して、再度deployします。
{
"app": "npx ts-node --prefer-ts-exts bin/cdk.ts",
"watch": {
},
"context": {
"desiredCount": 1,
% cd cdk
% cdk deploy mqtt-attach-acm-to-nlb-stack -c tag=latest
% cd ..
deployが完了すると、ESCの「タスク」タブで、1つの実行中タスクが確認できます。
ターゲットグループでは、ヘルスチェックが、Healthyとなっていることを確認できます。
3 動作確認
(1) MQTTクライアントによるPub/Sub
mosquittoのクライアントで、トピック/test をSubscribeしておきます。
% mosquitto_sub -p 8883 -h mqtt.alexa-dev.tokyo -t /test --tls-version tlsv1.2
HELLO
同じトピックに対してメッセージをPublishすると、Subscribe側で到着していることが確認できます。
% mosquitto_pub -p 8883 -h mqtt.alexa-dev.tokyo -t /test -m "HELLO" --tls-version tlsv1.2
-p 8883 を指定することでTLS通信となっています。サーバ名は、パブリックなものなので、CA証明書を指定したりする必要はありません。
(2) ECS Execによるログ確認
ECS Execでログインすると、/mosquitto/log/mosquitto.logでログを確認できます。
% aws ecs execute-command --region ap-northeast-1 --cluster mqtt-attach-acm-to-nlb-cluster --task ca4cfefe874543028c2f3d48516c5d9e --container mqtt-attach-acm-to-nlb-ServiceTaskContainerDefinition --interactive --command "/bin/sh"
The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.
Starting session with SessionId: ecs-execute-command-gh6gvpla4n7rvy9qqvuufy6vae
/ # vi /mosquitto/log/mosquitto.log
【起動時のログ】
1735619608: mosquitto version 2.0.20 starting
1735619608: Config loaded from /mosquitto/mosquitto.conf.
1735619608: Opening ipv4 listen socket on port 1883.
1735619608: Opening ipv6 listen socket on port 1883.
1735619608: mosquitto version 2.0.20 running
【Subscribeのログ】
1735620481: New connection from 10.0.0.90:12989 on port 1883.
1735620481: New client connected from 10.0.0.90:12989 as auto-851B7011-5703-58AC-807C-D1F38BA6A18D (p2, c1, k60).
1735620481: No will message specified.
1735620481: Sending CONNACK to auto-851B7011-5703-58AC-807C-D1F38BA6A18D (0, 0)
1735620481: Received SUBSCRIBE from auto-851B7011-5703-58AC-807C-D1F38BA6A18D
1735620481: /test (QoS 0)
1735620481: auto-851B7011-5703-58AC-807C-D1F38BA6A18D 0 /test
1735620481: Sending SUBACK to auto-851B7011-5703-58AC-807C-D1F38BA6A18D
【Publishのログ】
1735620485: New client connected from 10.0.0.90:51004 as auto-36D04964-20DB-8768-C557-90979FAF1860 (p2, c1, k60).
1735620485: No will message specified.
1735620485: Sending CONNACK to auto-36D04964-20DB-8768-C557-90979FAF1860 (0, 0)
1735620485: Received PUBLISH from auto-36D04964-20DB-8768-C557-90979FAF1860 (d0, q0, r0, m0, '/test', ... (5 bytes))
1735620485: Sending PUBLISH to auto-851B7011-5703-58AC-807C-D1F38BA6A18D (d0, q0, r0, m0, '/test', ... (5 bytes))
1735620485: Received DISCONNECT from auto-36D04964-20DB-8768-C557-90979FAF1860
1735620485: Client auto-36D04964-20DB-8768-C557-90979FAF1860 disconnected.
NLBは、接続元のIPアドレスを保持していないので、NLBからポート1883に対する接続となっていることが分かります。
4 削除
destroyで、CDKで作成されたリソースは、削除されます。
% cdk destroy mqtt-attach-acm-to-nlb-stack -c tag=latest
tag: latest
Are you sure you want to delete: mqtt-attach-acm-to-nlb-stack (y/n)? y
mqtt-attach-acm-to-nlb-stack: destroying... [1/1]
✅ mqtt-attach-acm-to-nlb-stack: destroyed
%
destroyで残ってしまう、ECRリポジトリ及びロググループについては、手動で削除してください。
また、元々手動で作成したACMの証明書や、Route 53のリソースについても、削除を忘れないでください。
5 補足説明
以下、一部補足の説明をさせてください。
(1) Dockerの内容
Dockerのイメージは、下記のファイルでbuildされています。
ベースイメージは、eclipse-mosquittoであり、mosquittoの設定ファイルは、別途作成したもの(後述)を使用しています。
Dockerfile
FROM eclipse-mosquitto
COPY mosquitto.conf /mosquitto/mosquitto.conf
COPY run.sh /run.sh
RUN chmod +x /run.sh
CMD /run.sh
run.sh
echo "Starting mosquitto"
/usr/sbin/mosquitto -c /mosquitto/mosquitto.conf -v
(2) Mosquittoの設定ファイル
設定ファイルは、以下のとおりです。listenerで1883を指定することで、TLS無しで起動します。
mosquitto.config
listener 1883
allow_anonymous true
log_dest file /mosquitto/log/mosquitto.log
log_type all
connection_messages true
各種モードでの、必要な設定は以下のような感じです。
設定値 | MQTT | MQTTS | MQTTS相互認証 | 備考 |
---|---|---|---|---|
listener | 1883 | 8883 | 8883 | 設定がない場合localhostのみ受け付ける |
keyfile | 必要 | 必要 | サーバ秘密鍵 | |
certfile | 必要 | 必要 | サーバ証明書 | |
cafile | 必要 | クライアントの証明書検証用 | ||
require_certificate | true | クライアント証明書を要求 |
※ allow_anonymous true (パスワード認証)
(3) NLBのTLS終端
CDKにおいて、NLBのTLS終端は、protocolにelbv2.Protocol.TLSを指定して、certificatesに証明書を設置することで行います。
// TLSでListenする場合
const nlbListener = nlbForApp.addListener(`nlbListener`, {
protocol: elbv2.Protocol.TLS,
port: 8883,
certificates: [
{
certificateArn: certificateArn,
},
],
});
参考までに、単なるTCPのリスナーであれば、下記のようになります。
// TLSなしでListenする場合
const nlbListener = nlbForApp.addListener(`nlbListener`, {
port: 1883,
});
6 最後に
今回は、ACMで発行した証明書で、MosquittoによるMQTTSサーバを構築してみました。
基本的にMQTTメッセージブローカーは、IoT Coreが利用できますが、既存システムの移行時に、IoT Coreでは、対応しきれず、Mosquittoで構築する場面もあるようです。
このような時に、証明書をACMで運用できれば、僅かでも管理面の負担が下がるかも知れません。
なお、ELBのTLS終端は、クライアント認証の機能が無いため、今回の仕組みは、TLS相互認証に対応できないことにご注意ください。(相互認証は、MosquittoでTLS終端するしかない)
ただし、パスワード認証は、アプリ層なので、利用可能なはずです。